#import "draw.h"

extern int errno;

#define DRAW_VERSION_2_0 184
#define DRAW_VERSION_3_0_PRERELEASE 234
#define DRAW_VERSION_3_0 245
#define NEW_DRAW_VERSION DRAW_VERSION_3_0

/* Localization strings */

#define SAVE NXLocalString("Save", NULL, "Button choice which allows the user to save his document.")
 
#define DONT_SAVE NXLocalString("Don't Save", NULL, "Button choice which allows the user to abort the saving of his document when the user has asked him if he'd like to do so.")
 
#define REVERT NXLocalString("REVERT_BUTTON", "Revert", "Button choice which allows the user to revert his changes to what was last saved on disk.")
 
@implementation DrawDocument
/*
 * This class is used to keep track of a Draw document.
 *
 * Its view and window instance variables keep track of the GraphicView
 * comprising the document as well as the window it is in.
 * The printInfo instance variable is used to allow the user to control
 * how the printed page is printed.  It is an instance of a PrintInfo
 * object (which is edited via the PageLayout and PrintPanels).
 * The listener is used to allow the user to drag an icon representing
 * a PostScript or TIFF file into the document.  The iconPathList is the
 * list of files which was last dragged into the document.
 * The name and directory specify where the document is to be saved.
 * haveSavedDocument keeps track of whether a disk file is associated
 * with the document yet (i.e. if it has ever been saved).
 *
 * The DrawDocument class's responsibilities:
 *
 * 1. Manage the window (including the scrolling view) which holds the
 *    document's GraphicView.  This includes constraining the resizing of
 *    the window so that it never becomes larger than the GraphicView, and
 *    ensuring that if the window contains an unsaved document and the user
 *    tries to close it, the user gets an opportunity to save her changes.
 * 2. Handle communication with the Workspace Manager which allows icons
 *    for PostScript and TIFF files to be dragged into the document window
 *    and be assimilated into the document.
 * 3. Saving the document to a disk file.
 * 4. Provide an external interface to saving the contents of the GraphicView
 *    as a PostScript or TIFF file.
 */

#define MIN_WINDOW_WIDTH 50.0
#define MIN_WINDOW_HEIGHT 75.0
#define SCROLLVIEW_BORDER NX_NOBORDER

static NXRect *calcFrame(PrintInfo *printInfo, NXRect *viewRect)
/*
 * Calculates the size of the page the user has chosen minus its margins.
 */
{
    float lm, rm, bm, tm;
    const NXRect *paperRect;

    viewRect->origin.x = viewRect->origin.y = 0.0;
    paperRect = [printInfo paperRect];
    [printInfo getMarginLeft:&lm right:&rm top:&tm bottom:&bm];
    viewRect->size = paperRect->size;
    viewRect->size.width -= lm + rm;
    viewRect->size.height -= tm + bm;

    return viewRect;
}

static void getContentSizeForView(View *view, NXSize *contentSize)
/*
 * Calculates the size of the window's contentView by accounting for the
 * existence of the ScrollView around the GraphicView.  No scrollers are
 * assumed since we are interested in the minimum size need to enclose
 * the entire view and, if the entire view is visible, we don't need
 * scroll bars!
 */
{
    NXRect viewFrame;

    [view getFrame:&viewFrame];
    [SyncScrollView getFrameSize:contentSize
	      forContentSize:&viewFrame.size
	       horizScroller:YES vertScroller:YES
		  borderType:SCROLLVIEW_BORDER];
}

#define WINDOW_MASK (NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK|NX_FLAGSCHANGEDMASK)

static Window *createWindowFor(View *view, NXRect *windowContentRect, const char *frameString)
/*
 * Creates a window for the specified view.
 * If windowContentRect is NULL, then a window big enough to fit the whole
 * view is created (unless that would be too big to comfortably fit on the
 * screen, in which case a smaller window may be allocated).
 * If windowContentRect is not NULL, then it is used as the contentView of
 * the newly created window.
 *
 * setMiniwindowIcon: sets the name of the bitmap which will be used in
 * the miniwindow of the window (i.e. when the window is miniaturized).
 * The icon "drawdoc" was defined in InterfaceBuilder (take a look in
 * the icon suitcase).
 */
{
    Window *window;
    NXSize screenSize;
    SyncScrollView *scrollView;
    NXRect defaultWindowContentRect;

    if (!windowContentRect) {
	windowContentRect = &defaultWindowContentRect;
	getContentSizeForView(view, &windowContentRect->size);
	[NXApp getScreenSize:&screenSize];
	if (windowContentRect->size.width > screenSize.width / 2.0) {
	    windowContentRect->size.width = floor(screenSize.width / 2.0);
	}
	if (windowContentRect->size.height > screenSize.height - 20.0) {
	    windowContentRect->size.height = screenSize.height - 20.0;
	}
	windowContentRect->origin.x = screenSize.width - 85.0 - windowContentRect->size.width;
	windowContentRect->origin.y = floor((screenSize.height - windowContentRect->size.height) / 2.0);
    }

    window = [[Window allocFromZone:[view zone]] initContent:windowContentRect
			  style:NX_RESIZEBARSTYLE
		        backing:(InMsgPrint ? NX_NONRETAINED : NX_BUFFERED)
		     buttonMask:NX_CLOSEBUTTONMASK|NX_MINIATURIZEBUTTONMASK
			  defer:(InMsgPrint ? NO : YES)];

    if (frameString) [window setFrameFromString:frameString];
    scrollView = [[SyncScrollView allocFromZone:[view zone]] initFrame:windowContentRect];
    [scrollView setRulerClass:[Ruler class]];
    [scrollView setRulerOrigin:UpperLeft];
    [scrollView setRulerWidths:[Ruler width] :[Ruler width]];
    [scrollView setVertScrollerRequired:YES];
    [scrollView setHorizScrollerRequired:YES];
    [scrollView setBorderType:SCROLLVIEW_BORDER];
    [scrollView setDocView:view];
    [[window setContentView:scrollView] free];
    [window addToEventMask:WINDOW_MASK];
    [window makeFirstResponder:view];
    [window setMiniwindowIcon:"drawdoc"];
    [window setFreeWhenClosed:YES];

    return window;
}

static int removeFile(const char *file)
{
    DIR *dirp;
    struct stat st;
    struct direct *dp;
    char *leaf = NULL;
    char path[MAXPATHLEN+1];

    if (!stat(file, &st)) {
	if ((st.st_mode & S_IFMT) == S_IFDIR) {
	    dirp = opendir(file);
	    for (dp = readdir(dirp); dp != NULL; dp = readdir(dirp)) {
		if (strcmp(dp->d_name, ".") && strcmp(dp->d_name, "..")) {
		    if (!leaf) {
			strcpy(path, file);
			strcat(path, "/");
			leaf = path + strlen(path);
		    }
		    strcpy(leaf, dp->d_name);
		    if (unlink(path)) {
			closedir(dirp);
			return -1;
		    }
		}
	    }
	    return rmdir(file);
	} else {
	    return unlink(file);
	}
    }

    return -1;
}

/* Very private methods needed by factory methods */

+ backupOldDrawDocument:(const char *)file
/*
 * We do this because this is not a fully-supported application.
 */
{
    NXStream *volatile stream;
    char *extension, *dash;
    NXTypedStream *volatile ts = NULL;
    volatile int version = NEW_DRAW_VERSION;
    char buffer[MAXPATHLEN+1];

    NX_DURING
	stream = NXMapFile(file, NX_READONLY);
	if (stream) ts = NXOpenTypedStream(stream, NX_READONLY);
	if (ts) NXReadType(ts, "i", (int *)&version);
    NX_HANDLER
    NX_ENDHANDLER

    if (ts) NXCloseTypedStream(ts);
    if (stream) NXCloseMemory(stream, NX_FREEBUFFER);

    if (version < DRAW_VERSION_3_0_PRERELEASE) {
	strcpy(buffer, file);
	extension = strrchr(buffer, '.');
	if (extension && !strcmp(extension, ".draw")) {
	    dash = strrchr(buffer, '-');
	    if (version == DRAW_VERSION_2_0) {
		if (!dash || strcmp(dash, "-2.0.draw")) strcpy(extension, "-2.0.draw");
	    } else {
		if (!dash || strcmp(dash, "-1.0.draw")) strcpy(extension, "-1.0.draw");
	    }
	} else {
	    if (version == DRAW_VERSION_2_0) {
		strcat(buffer, "-2.0");
	    } else {
		strcat(buffer, "-1.0");
	    }
	}
	link(file, buffer);
    }

    return self;
}

- (BOOL)loadDocument:(NXStream *)stream frameSize:(NXRect *)frame frameString:(char *)frameString
/*
 * Loads an archived document from the specified filename.
 * Loads the window frame specified in the archived document into the
 * frame argument (if the frame argument is NULL, then the frame in
 * the archived document is ignored).  Returns YES if the document
 * has been successfully loaded, NO otherwise.  Note that this method
 * destroys the receiving document, so use with extreme care
 * (essentially, this should only be called when a new document is
 * being created or an existing one is being reverted to its form
 * on disk).
 *
 * An NX_DURING handler is needed around the NXTypedStream operations because
 * if the user has asked that a bogus file be opened, the NXTypedStream will
 * raise an error.  To handle the error, the NXTypedStream must be closed.
 */
{
    char *s;
    int cgi, version;
    volatile NXRect docFrame;
    volatile BOOL retval = YES;
    NXTypedStream *volatile ts = NULL;

    NX_DURING
	ts = NXOpenTypedStream(stream, NX_READONLY);
	if (ts) {
	    NXSetTypedStreamZone(ts, [self zone]);
	    NXReadType(ts, "i", &version);
	    printInfo = NXReadObject(ts);
	    if (version >= DRAW_VERSION_3_0_PRERELEASE) {
		NXReadType(ts, "*", &s);
		if (frameString) strcpy(frameString, s);
		free(s);
	    } else {
		NXReadRect(ts, (NXRect *)&docFrame);
	    }
	    if (version >= DRAW_VERSION_3_0) {
		NXReadType(ts, "i", &cgi);
		[Graphic updateCurrentGraphicIdentifier:cgi];
	    }
	    view = NXReadObject(ts);
	} else {
	    retval = NO;
	}
    NX_HANDLER
	retval = NO;
    NX_ENDHANDLER

    if (ts) NXCloseTypedStream(ts);
    if (retval && frame) *frame = docFrame;

    return retval;
}

/* Factory methods */

/*
 * We reuse zones since it doesn't cost us anything to have a
 * zone lying around (e.g. if we open ten documents at the start
 * then don't use 8 of them for the rest of the session, it doesn't
 * cost us anything except VM (no real memory cost)), and it is
 * risky business to go around NXDestroy()'ing zones since if
 * your application accidentally allocates some piece of global
 * data into a zone that gets destroyed, you could have a pointer
 * to freed data on your hands!  We use the List object since it
 * is so easy to use (which is okay as long as 'id' remains a
 * pointer just like (NXZone *) is a pointer!).
 *
 * Note that we don't implement alloc and allocFromZone: because
 * we create our own zone to put ourselves in.  It is generally a
 * good idea to "notImplemented:" those methods if you do not allow
 * an object to be alloc'ed from an arbitrary zone (other examples
 * include Application and all of the Application Kit panels
 * (which allocate themselves into their own zone).
 */

static List *zoneList = nil;

+ (NXZone *)newZone
{
    if (!zoneList || ![zoneList count]) {
	return NXCreateZone(vm_page_size, vm_page_size, YES);
    } else {
	return (NXZone *)[zoneList removeLastObject];
    }
}

+ (void)reuseZone:(NXZone *)aZone
{
    if (!zoneList) zoneList = [List new];
    [zoneList addObject:(id)aZone];
    NXNameZone(aZone, "Unused");
}

+ allocFromZone:(NXZone *)aZone
{
    return [self notImplemented:@selector(allocFromZone:)];
}

+ alloc
{
    return [self notImplemented:@selector(alloc)];
}

/* Creation methods */

+ new
/*
 * Creates a new, empty, document.
 *
 * Creates a PrintInfo object; creates a view whose size depends on the
 * default PrintInfo created; creates a window for that view; sets self
 * as the window's delegate; orders the window front; registers the window
 * with the Workspace Manager.  Note that the default margins are set
 * to 1/2 inch--that's more appropriate for a draw program than 1 or 1.25
 * inches.
 */
{
    NXZone *zone;
    NXRect frameRect;

    zone = [self newZone];
    self = [super allocFromZone:zone];
    [self init];
    printInfo = [[PrintInfo allocFromZone:zone] init];
    [printInfo setMarginLeft:36.0 right:36.0 top:36.0 bottom:36.0];
    calcFrame(printInfo, &frameRect);
    view = [[GraphicView allocFromZone:zone] initFrame:&frameRect];
    [view setClipping:NO];			/* since it is in a ClipView */
    window = createWindowFor(view, NULL, NULL);
    [window setDelegate:self];
    [self resetScrollers];
    [self setName:NULL andDirectory:NULL];
    [self setLinkManager:[[NXDataLinkManager allocFromZone:[self zone]] initWithDelegate:self]];
    [window makeKeyAndOrderFront:self];

    return self;
}

+ newFromStream:(NXStream *)stream
/*
 * Creates a new document from what is in the passed stream.
 */
{
    NXZone *zone;
    NXRect contentViewFrame;
    char frameString[NX_MAXFRAMESTRINGLENGTH];

    zone = [self newZone];
    self = [super allocFromZone:zone];
    [self init];
    *frameString = '\0';	// will come back still "" if an old file is read
    if (stream && [self loadDocument:stream frameSize:&contentViewFrame frameString:frameString]) {
	window = createWindowFor(view, &contentViewFrame, *frameString ? frameString : NULL);
	[window setDelegate:self];
	[self resetScrollers];
	haveSavedDocument = YES;
	return self;
    } else {
	NXRunLocalizedAlertPanel(NULL, "Open Draw Document", "I/O error.  Can't open file.", NULL, NULL, NULL, "Alert given to user when he tries to open a draw document and there is an I/O error.  This usually happens when the document being opened is not really a draw document but somehow got renamed to have the .draw extension.");
	[self free];
	return nil;
    }
}

+ newFromFile:(const char *)file andDisplay:(BOOL)display
/*
 * Opens an existing document from the specified file.
 */
{
    struct stat st;
    NXStream *stream = NULL;
    DrawDocument *newDocument;
    char cwd[MAXPATHLEN+1], path[MAXPATHLEN+1];

    [self backupOldDrawDocument:file];

    if (!stat(file, &st)) {
	if ((st.st_mode & S_IFMT) == S_IFDIR) {
	    getwd(cwd);
	    if (!chdir(file) && getwd(path)) {
		stream = NXMapFile("document.draw", NX_READONLY);
	    } else {
		strcpy(path, file);
		strcat(path, "/document.draw");
		stream = NXMapFile(path, NX_READONLY);
		strcpy(path, file);
	    }
	    chdir(cwd);
	} else {
	    stream = NXMapFile(file, NX_READONLY);
	    strcpy(path, file);
	}
    }

    if (stream) {
	if (newDocument = [self newFromStream:stream]) {
	    [newDocument setName:path];
	    [newDocument setLinkManager:[[NXDataLinkManager allocFromZone:[newDocument zone]] initWithDelegate:newDocument fromFile:path]];
	    if ([newDocument isDirty]) [newDocument dirty:nil];	// initWithDelegate:fromFile: might dirty our document but the linkManager is obviously not set yet, so let it know now that it is set, catch-22!
	    if (display) [newDocument->window makeKeyAndOrderFront:newDocument];
	}
	NXCloseMemory(stream, NX_FREEBUFFER);
	return newDocument;
    } else {
	NXRunLocalizedAlertPanel(NULL, "Open Draw Document", "I/O error.  Can't open file.", NULL, NULL, NULL, "Alert given to user when he tries to open a draw document and there is an I/O error.  This usually happens when the document being opened is not really a draw document but somehow got renamed to have the .draw extension.");
	return nil;
    }
}

+ newFromFile:(const char *)file
{
    return [self newFromFile:file andDisplay:YES];
}

- init
{
    [super init];
    [self registerForServicesMenu];
    return self;
}

- free
{
    [self reset:self];
    if ([NXApp printInfo] == printInfo) [NXApp setPrintInfo:nil];
    [printInfo free];
    [linkManager free];
    NX_FREE(name);
    NX_FREE(directory);
    NX_FREE(iconPathList);
    [[self class] reuseZone:[self zone]];
    return [super free];
}

/* Data link methods -- see gvLinks.m and Links.rtf for more info. */

- setLinkManager:(NXDataLinkManager *)aLinkManager
{
    linkManager = aLinkManager;
    [view setLinkManager:aLinkManager];
    return self;
}

- showSelection:(NXSelection *)selection
{
    return [view showSelection:selection];
}

- copyToPasteboard:(Pasteboard *)pasteboard at:(NXSelection *)selection cheapCopyAllowed:(BOOL)flag
{
    return [view copyToPasteboard:pasteboard at:selection cheapCopyAllowed:flag];
}

- pasteFromPasteboard:(Pasteboard *)pasteboard at:(NXSelection *)selection
{
    return [view pasteFromPasteboard:pasteboard at:selection];
}

- importFile:(const char *)filename at:(NXSelection *)selection
{
    return [view importFile:filename at:selection];
}

- windowForSelection:(NXSelection *)selection
{
    return window;
}

- dataLinkManager:linkManager didBreakLink:(NXDataLink *)aLink
{
    return [view breakLinkAndRedrawOutlines:aLink];
}

- dataLinkManagerRedrawLinkOutlines:(NXDataLinkManager *)sender
{
    return [view breakLinkAndRedrawOutlines:nil];
}

- (BOOL)dataLinkManagerTracksLinksIndividually:(NXDataLinkManager *)sender
{
    return YES;
}

- dataLinkManager:(NXDataLinkManager *)sender startTrackingLink:(NXDataLink *)link
{
    [view startTrackingLink:link];
    return self;
}

- dataLinkManager:(NXDataLinkManager *)sender stopTrackingLink:(NXDataLink *)link
{
    [view stopTrackingLink:link];
    return self;
}

- dataLinkManagerDidEditLinks:(NXDataLinkManager *)sender
{
    [self dirty:self];
    [view updateLinksPanel];
    return self;
}

- saveLink:sender
{
    NXSelection *selection;
    NXDataLink *link;

    selection = [view currentSelection];
    link = [[NXDataLink alloc] initLinkedToSourceSelection:selection managedBy:linkManager supportingTypes:TypesDrawExports() count:NUM_TYPES_DRAW_EXPORTS];
    [link saveLinkIn:[self filename]];
    [link free];

    return self;
}

/*
 * Overridden from ChangeManager (Undo stuff)
 */

- changeWasDone
{
    [super changeWasDone];
    [window setDocEdited:[self isDirty]];
    [linkManager documentEdited];
    return self;
}

- changeWasUndone
{
    [super changeWasUndone];
    [window setDocEdited:[self isDirty]];
    [linkManager documentEdited];
    return self;
}

- changeWasRedone
{
    [super changeWasRedone];
    [window setDocEdited:[self isDirty]];
    [linkManager documentEdited];
    return self;
}

- clean:sender
{
    [super clean:sender];
    [window setDocEdited:NO];
    return self;
}

- dirty:sender
{
    [super dirty:sender];
    [window setDocEdited:YES];
    [linkManager documentEdited];
    return self;
}

/* Services menu support methods. */

/* Services menu registrar */

- registerForServicesMenu
{
    static BOOL registered = NO;
    const char *validSendTypes[2];

    if (!registered) {
	registered = YES;
	validSendTypes[0] = NXFilenamePboardType;
	validSendTypes[1] = NULL;
	[NXApp registerServicesMenuSendTypes:validSendTypes andReturnTypes:NULL];
    }

    return self;
}

- validRequestorForSendType:(NXAtom)sendType andReturnType:(NXAtom)returnType
/*
 * Services menu support.
 * We are a valid requestor if the send type is filename
 * and there is no return data from the request.
 */
{
    return (haveSavedDocument && sendType == NXFilenamePboardType && (!returnType || !*returnType)) ? self : nil;
}

#define SERVICE NXLocalString("Service", NULL, "This is the the title of an alert which comes up when the user requests a service from the Services menu.")
#define SAVE_FOR_SERVICE NXLocalString("Do you wish to save this document before your request is serviced?", NULL, "This question appears in an alert when the user requests a service from the Services menu which operates on the entire draw document, but the draw document has been edited since it was last saved.  Draw is just asking if the user would like to save the document first so that the service will operate on the current state of the document rather than on the last saved state.")

- writeSelectionToPasteboard:pboard types:(NXAtom *)types
/*
 * Services menu support.
 * Here we are asked by the Services menu mechanism to supply
 * the filename (which we said we were a valid requestor for
 * in the above method).
 */
{
    int save;

    if (haveSavedDocument) {
	while (types && *types) if (*types == NXFilenamePboardType) break; else types++;
	if (types && *types) {
	    if ([self isDirty]) {
		save = NXRunAlertPanel(SERVICE, SAVE_FOR_SERVICE, SAVE, DONT_SAVE, NULL);
		if (save == NX_ALERTDEFAULT) {
		    if ([self save]) [linkManager documentSaved];
		}
	    }
	    [pboard declareTypes:&NXFilenamePboardType num:1 owner:self];
	    [pboard writeType:NXFilenamePboardType data:[self filename] length:strlen([self filename])+1];
	    return self;
	}
    }

    return nil;
}

/* Other methods. */

- resetScrollers
/*
 * Checks to see if the new window size is too large.
 * Called whenever the page layout (either by user action or
 * by the opening or reverting of a file) is changed or
 * the user resizes the window.
 */
{
    SyncScrollView *scrollView;
    NXSize contentSize;
    NXRect contentRect, windowFrame;
    BOOL updateRuler = NO;

    if (window) {
	[window getFrame:&windowFrame];
	[[window class] getContentRect:&contentRect
			  forFrameRect:&windowFrame
				 style:[window style]];
	scrollView = [window contentView];
	getContentSizeForView(view, &contentSize);
	if ([scrollView horizontalRulerIsVisible]) {
	    contentSize.height += [Ruler width];
	    updateRuler = YES;
	}
	if ([scrollView verticalRulerIsVisible]) {
	    contentSize.width += [Ruler width];
	    updateRuler = YES;
	}
	if (contentRect.size.width >= contentSize.width || contentRect.size.height >= contentSize.height) {
	    contentSize.width = MIN(contentRect.size.width, contentSize.width);
	    contentSize.height = MIN(contentRect.size.height, contentSize.height);
	    [window sizeWindow:contentSize.width :contentSize.height];
	}
	if (updateRuler) [scrollView updateRuler];
    }

    return self;
}

- view
/*
 * Returns the GraphicView associated with this document.
 */
{
    return view;
}

- printInfo
/*
 * Returns the PrintInfo object associated with this document.
 */
{
    return printInfo;
}

/* Target/Action methods */

#define BAD_MARGINS NXLocalString("The margins or paper size specified are invalid.", NULL, NULL)

- changeLayout:sender
/*
 * Puts up a PageLayout panel and allows the user to pick a different
 * size paper to work on.  After she does so, the view is resized to the
 * new paper size.
 * Since the PrintInfo is effectively part of the document, we note that
 * the document is now dirty (by performing the dirty method).
 */
{
    NXRect frame;
    float lm, rm, tm, bm;
    float savedlm, savedrm, savedtm, savedbm;
    const NXRect *paperRect;

    [printInfo getMarginLeft:&savedlm right:&savedrm top:&savedtm bottom:&savedbm];
    if ([[NXApp pageLayout] runModal] == NX_OKTAG) {
	paperRect = [printInfo paperRect];
	[printInfo getMarginLeft:&lm right:&rm top:&tm bottom:&bm];
	if (lm < 0.0 || rm < 0.0 || tm < 0.0 || bm < 0.0 ||
	    paperRect->size.width - lm - rm < 0.0 || paperRect->size.height - tm - bm < 0.0) {
	    [printInfo setMarginLeft:savedlm right:savedrm top:savedtm bottom:savedbm];
	    NXRunAlertPanel(NULL, BAD_MARGINS, NULL, NULL, NULL);
	    return self;
	}
	calcFrame(printInfo, &frame);
	[view sizeTo:frame.size.width :frame.size.height];
	[self resetScrollers];
	[view display];
	[self dirty:self];
    }

    return self;
}

- changeGrid:sender
/*
 * Changes the grid by putting up a modal panel asking the user what
 * she wants the grid to look like.
 */
{
    [[NXApp gridInspector] runModalForGraphicView:view];
    return self;
}

- close:sender
{
    [window performClose:self];
    return self;
}

- save:sender
/*
 * Saves the file.  If this document has never been saved to disk,
 * then a SavePanel is put up to ask the user what file name she
 * wishes to use to save the document.
 */
{
    if (haveSavedDocument) {
        if ([self save]) {
	    [linkManager documentSaved];
	    [self clean:self];
	}
	return self;
    } else {
        return [self saveAs:sender];
    }
}

- saveAs:sender
{
    struct stat st;
    const char *spfname;
    SavePanel *savepanel;
    char cwd[MAXPATHLEN+1], path[MAXPATHLEN+1];

    savepanel = [NXApp saveAsPanel:sender];
    if ([savepanel runModalForDirectory:directory file:name]) {
	getwd(cwd);
	spfname = [savepanel filename];
	if (stat(spfname, &st) && errno == ENOENT) mkdir(spfname, 0755);
	if (chdir(spfname) || !getwd(path)) strcpy(path, spfname);
	chdir(cwd);
	[self setName:path];
	if ([self save]) {
	    [linkManager documentSavedAs:path];
	    [self clean:self];
	}
	return self;
    } 
    
    return nil;
}

- changeSaveType:sender
/*
 * Called by the SavePanel accessory view whenever the user chooses
 * a different type of file to save to.  The window of the sender
 * is, of course, the SavePanel itself.  setRequiredFileType: does
 * not affect the SavePanel while it is running.  It only has effect
 * when the user has chosen a file, and the SavePanel ensures that it
 * has the correct extension by adding it if it doesn't have it already.
 * This message gets here via the Responder chain from the SavePanel.
 */
{
    switch ([sender selectedRow]) {
	case 0: [[sender window] setRequiredFileType:"draw"]; break;
	case 1: [[sender window] setRequiredFileType:"eps"]; break;
	case 2: [[sender window] setRequiredFileType:"tiff"]; break;
    }
    return self;
}

- saveTo:sender
/*
 * This takes the document and saves it as a Draw document file, PostScript
 * file, or TIFF file.  If the document type chosen is Draw document, then
 * this saves the file, but DOES NOT make that file the currently edited
 * file (this makes it easy to save your document elsewhere as a backup
 * and keep on going in the current document).
 *
 * If PostScript or TIFF is selected, then the document is written out
 * in the appropriate format.  In the case of PostScript and TIFF, the
 * actual saving is done using the more general method saveAs:using:.
 */
{
    const char *file;
    SavePanel *savepanel;
    char *type, *savedName, *savedDirectory;
    BOOL reallyHaveSavedDocument;
    char buffer[MAXPATHLEN+1];

    strcpy(buffer, name);
    type = strrchr(buffer, '.');
    if (type) *type = '\0';
    savepanel = [NXApp saveToPanel:sender];
    if (![savepanel runModalForDirectory:directory file:buffer]) return self;

    file = [savepanel filename];
    type = strrchr(file, '.');
    if (type) {
	if (!strcmp(type, ".eps")) {
	    [self saveTo:file using:@selector(writePSToStream:)];
	} else if (!strcmp(type, ".tiff")) {
	    [self saveTo:file using:@selector(writeTIFFToStream:)];
	} else if (!strcmp(type, ".draw")) {
	    reallyHaveSavedDocument = haveSavedDocument;
	    savedName = name;			/* save current name */
	    savedDirectory = directory;		/* save current directory */
	    name = NULL; directory = NULL;	/* clear current filename */
	    [self setName:file];		/* temporarily change name */
	    if ([self save]) {			/* save, then restore name */
		[linkManager documentSavedTo:[self filename]];
	    }
	    [self setName:savedName andDirectory:savedDirectory];
	    haveSavedDocument = reallyHaveSavedDocument;
	}
    }

    return self;
}

#define REVERT_TITLE NXLocalString("Revert", NULL, "This is the title of the alert which asks the user if he is sure he wants to revert a document he has edited back to its last-saved state.")
#define SURE_TO_REVERT NXLocalString("%s has been edited.  Are you sure you want to undo changes?", NULL, "This question is asked of the user when he asks to revert the state of his document to the version last saved on disk, but he has made changes in the interim which would be lost if he did actually revert.  The %s is the name of the document.")

- revertToSaved:sender
/*
 * Revert the document back to what is on the disk.
 */ 
{
    struct stat st;
    const char *file;
    NXStream *stream = NULL;
    NXRect viewFrame, visibleRect;
    char path[MAXPATHLEN+1];

    if (!haveSavedDocument || ![self isDirty] || (NXRunAlertPanel(REVERT_TITLE, SURE_TO_REVERT, REVERT, CANCEL, NULL, name) != NX_ALERTDEFAULT)) {
	return self;
    }

    [view getVisibleRect:&visibleRect];
    [window endEditingFor:nil];

    file = [self filename];
    if (!stat(file, &st)) {
	if ((st.st_mode & S_IFMT) == S_IFDIR) {
	    strcpy(path, file);
	    strcat(path, "/document.draw");
	    stream = NXMapFile(path, NX_READONLY);
	} else {
	    stream = NXMapFile(file, NX_READONLY);
	}
    }

    if (stream && [self loadDocument:stream frameSize:NULL frameString:NULL]) {
        [self reset:self];
	[[[window contentView] setDocView:view] free];
	calcFrame(printInfo, &viewFrame);
	[window disableFlushWindow];
	[view sizeTo:viewFrame.size.width :viewFrame.size.height];
	[self resetScrollers];
	[view scrollRectToVisible:&visibleRect];
	[view display];
	[window reenableFlushWindow];
	[window flushWindow];
	[window makeFirstResponder:view];
	[self reset:self];
	[window setDocEdited:NO];
	[view setLinkManager:linkManager];
	[linkManager documentReverted];
	[view updateLinksPanel];
	NXCloseMemory(stream, NX_FREEBUFFER);
    } else {
	if (stream) NXCloseMemory(stream, NX_FREEBUFFER);
	NXRunLocalizedAlertPanel(NULL, "Revert", "I/O error.  Can't revert.", NULL, NULL, NULL, "This very rare alert shows up when the user tries to revert the state of his document to whatever it was when he opened it up.  Some unexpected I/O error occurs, however, and he is unable to do so.");
    }

    return self;
}

- showTextRuler:sender
/*
 * Sent to cause the Text object ruler to be displayed.
 * Only does anything if the rulers are already visible.
 */
{
    SyncScrollView *scrollView = [window contentView];

    if ([scrollView verticalRulerIsVisible] && [scrollView horizontalRulerIsVisible]) {
	[scrollView showHorizontalRuler:NO];
	[sender toggleRuler:sender];
    }

    return self;
}

- hideRuler:sender
/*
 * If sender is nil, we assume the sender wants the
 * ruler hidden, otherwise, we toggle the ruler.
 * If sender is the field editor itself, we do nothing
 * (this allows the field editor to demand that the
 * ruler stay up).
 */
{
    SyncScrollView *scrollView = [window contentView];
    Text *fe = [window getFieldEditor:NO for:NXApp];

    if (!sender && [scrollView verticalRulerIsVisible]) {
	[fe toggleRuler:sender];
	[window disableDisplay];
	[scrollView toggleRuler:nil];
	if ([scrollView verticalRulerIsVisible]) [scrollView showHorizontalRuler:YES];
	[window reenableDisplay];
	[scrollView resizeSubviews:(NXSize *)0];
    } else if (sender) {
	if ([scrollView verticalRulerIsVisible]) {
	    [scrollView showVerticalRuler:NO];
	    [scrollView showHorizontalRuler:NO];
	    if (![fe window]) [scrollView toggleRuler:nil];
	} else {
	    [scrollView showVerticalRuler:YES];
	    if ([fe window]) {
		[scrollView showHorizontalRuler:NO];
	    } else {
		[scrollView showHorizontalRuler:YES];
		[scrollView toggleRuler:nil];
	    }
	}
	if ([fe superview] != nil)
	    [fe toggleRuler:sender];
    }

    return self;
}

/* Methods related to naming/saving this document. */

- (const char *)filename
/*
 * Gets the fully specified file name of the document.
 * If directory is NULL, then the currentDirectory is used.
 * If name is NULL, then the default title is used.
 */
{
    static char filenamebuf[MAXPATHLEN+1];

    if (!directory && !name) {
	[self setName:NULL andDirectory:NULL];
    }
    if (directory) {
	strcpy(filenamebuf, directory);
	strcat(filenamebuf, "/");
    } else {
	filenamebuf[0] = '\0';
    }
    if (name) {
	strcat(filenamebuf, name);
    }

    return filenamebuf;
}

- (const char *)directory
{
    return directory;
}

- (const char *)name
{
    return name;
}

- setName:(const char *)newName andDirectory:(const char *)newDirectory
/*
 * Updates the name and directory of the document.
 * newName or newDirectory can be NULL, in which case the name or directory
 * will not be changed (unless one is currently not set, in which case
 * a default name will be used).
 */
{
    char oldName[MAXPATHLEN+1];

    if (directory && name) {
	strcpy(oldName, [self filename]);
    } else {
	oldName[0] = '\0';
    }

    if ((newName && *newName) || !name) {
 	if (!newName || !*newName) newName = NXLocalString("UNTITLED", NULL, "The name of a document which the user has not yet given a name to.");
	NX_FREE(name);
	name = NXCopyStringBufferFromZone(newName, [self zone]);
    }

    if ((newDirectory && (*newDirectory == '/')) || !directory) {
 	if (!newDirectory || (*newDirectory != '/')) {
	    newDirectory = [NXApp currentDirectory];
	}
	NX_FREE(directory);
	directory = NXCopyStringBufferFromZone(newDirectory, [self zone]);
    }

    [window setTitleAsFilename:[self filename]];
    NXNameZone([self zone], [self filename]);

    return self;
}

- (BOOL)setName:(const char *)file
/*
 * If file is a full path name, then both the name and directory of the
 * document is updated appropriately, otherwise, only the name is changed.
 */
{
    char *lastComponent;
    char path[MAXPATHLEN+1];

    if (file) {
	strcpy(path, file);
	lastComponent = strrchr(path, '/');
	if (lastComponent) {
	    *lastComponent++ = '\0';
	    [self setName:lastComponent andDirectory:path];
	    return YES;
	} else {
	    [self setName:file andDirectory:NULL];
	    return YES;
	}
    }

    return NO;
}

- setTemporaryTitle:(const char *)title
{
    [window setTitle:title];
    haveSavedDocument = NO;
    NX_FREE(directory);
    NX_FREE(name);
    directory = NXCopyStringBufferFromZone(NXHomeDirectory(), [self zone]);
    name = NXCopyStringBufferFromZone(title, [self zone]);
    return self;
}

- saveTo:(const char *)file using:(SEL)streamWriter
/*
 * Performed by the saveTo: method, this method uses the streamWriter method
 * to have the GraphicView write itself in some foreign format (i.e., not
 * in Draw archive format).  It does some work to make the default name
 * of the file being saved to be the name of the document with the appropriate
 * extension.  It brings up the SavePanel so the user can specify the name
 * of the file to save to.
 */
{
    NXStream *stream;

    if (!file || !streamWriter) return self;

    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
    if (stream) {
	[view perform:streamWriter with:(id)stream];
	NXSaveToFile(stream, file);
	NXCloseMemory(stream, NX_FREEBUFFER);
    }

    return self;
}

- save
/*
 * Writes out the document in three steps:
 *
 * 1. Write the printInfo object
 * 2. Write the frame of the window
 * 3. Write the GraphicView itself.
 * 4. Write form information if necessary
 *
 * See GraphicView's write: method for more details on how the GraphicView
 * is archived.
 *
 * Note the "This is questionable" line.  It's questionable because it means
 * that Draw documents saved under a particular SYSTEM version can really only
 * be opened under the same SYSTEM version no matter which version of DRAW
 * writes the file.  In other words, if we take 2.0 Draw on a 3.0 system and
 * save a file, that file will NOT be able to be opened with the same version
 * of Draw (2.0) on a 2.0 system.  The bottom line is that archiving Application
 * Kit objects as part of the file format of your documents ties the user forever
 * not only to the version of Draw that saved the file (obviously you cannot open
 * a file saved with Draw version 3.0 with Draw version 2.0--that's okay, we'll
 * accept that restriction), but also to the version of the system (meaning a file
 * saved with Draw 2.0 may not be openable by Draw 2.0 if the save and read don't
 * occur on the same version of NeXTSTEP).  Since Draw is always "re-released"
 * (for free, no less) whenever new versions of NeXTSTEP are released, this is
 * probably okay for Draw, but probably IS NOT OKAY for 3rd party applications!
 *
 * The right way to do it is just to have the GraphicView write out its data
 * directly to the typedstream (i.e. don't call NXWriteObject on the GraphicView,
 * just call a method you invent called writeToTypedStream: in GraphicView--your
 * implementation of writeToTypedStream: in GraphicView must remember to explicitly
 * write out any instance variables of View that need to be saved and restored
 * when the document is saved and reopened).
 *
 * The writing out of the PrintInfo object is more problematic.  There is no real
 * way for you to know how to write that out yourself.  NeXT will have to be careful
 * not to change the archiving of this object in an incompatible way (backwards or
 * forwards).
 */
{
    char *s;
    struct stat st;
    int cgi, version;
    volatile BOOL savedOk = NO;
    NXTypedStream *ts = NULL;
    const char *saveFile = [self filename];
    BOOL alreadyHasFormEntries = NO;
    char buffer[MAXPATHLEN+1], cwd[MAXPATHLEN+1];

    if (!stat(saveFile, &st) && (st.st_mode & S_IFMT) != S_IFDIR) unlink(saveFile); // remove 1.0/2.0 files
    errno = 0;
    if (getwd(cwd) && (!chdir(saveFile) || errno == ENOENT)) {
	if (errno != ENOENT) {
	    alreadyHasFormEntries = !stat("form.info", &st);
	    getwd(buffer);
	} else {
	    strcpy(buffer, saveFile);
	}
	if (s = strrchr(buffer, '/')) {
	    *s++ = '\0';
	    chdir(buffer);
	    s = NXCopyStringBufferFromZone(s, [self zone]);
	} else {
	    s = NXCopyStringBufferFromZone(saveFile, [self zone]);
	}
	strcpy(buffer, s);
	strcat(buffer, "~");
	if ((removeFile(buffer) && errno != ENOENT) || (rename(s, buffer) && errno != ENOENT)) {
	    NXRunLocalizedAlertPanel(NULL, "Save", "Can't create backup file.", NULL, NULL, NULL);
	} else if (!mkdir(s, 0755)) {
	    chdir(s);
	    ts = NXOpenTypedStreamForFile("document.draw", NX_WRITEONLY);
	}
	free(s);
	if (ts) {
	    NX_DURING
		version = NEW_DRAW_VERSION;
		[window makeFirstResponder:view];
		NXWriteType(ts, "i", &version);
		NXWriteRootObject(ts, printInfo);
		[window saveFrameToString:(s = buffer)];
		NXWriteType(ts, "*", &s);
		cgi = [Graphic currentGraphicIdentifier];
		NXWriteType(ts, "i", &cgi);
		NXWriteRootObject(ts, view);	// This is questionable. See above.
		NXCloseTypedStream(ts);
		savedOk = YES;
		haveSavedDocument = YES;
		if ([view hasFormEntries]) {
		    if (alreadyHasFormEntries || NXRunLocalizedAlertPanel(NULL, "Save", "This document has Form Entries in it.  Do you wish to save them with the document?", "Yes", "No", NULL)) {
			[view writeFormEntriesToFile:"form.info"];
			strcpy(buffer, saveFile);
			strcat(buffer, "/form.eps");
			[self saveTo:buffer using:@selector(writePSToStream:)];
		    }
		}
	    NX_HANDLER
	    NX_ENDHANDLER
	}
    }

    chdir(cwd);

    if (!savedOk) NXRunLocalizedAlertPanel(NULL, "Save", "Can't save file.", NULL, NULL, NULL, "This alert appears when the user has asked to save his file somewhere, but Draw was unable to create that file.  This can occur for many reasons, the most common of which is that the file or directory is not writable.");

    return savedOk ? self : nil;
}

- (BOOL)isSameAs:(const char *)filename
{
    struct stat me, other;
    if (!stat([self filename], &me) && !stat(filename, &other) &&
	me.st_dev == other.st_dev && me.st_ino == other.st_ino) return YES;
    return NO;
}

/* Window delegate methods. */

#define SAVE_CHANGES NXLocalString("%s has changes. Save them?", NULL, "Question asked of user when he tries to close a window, either by choosing close from the menu or clicking the close box or quitting the application, and the contents of the window have not been saved.  The %s is the name of the document.")

- windowWillClose:sender cancellable:(BOOL)cancellable
/*
 * If the GraphicView has been edited, then this asks the user if she
 * wants to save the changes before closing the window.
 *
 * Returning nil from this method informs the caller that the window should
 * NOT be closed.  Anything else implies it should be closed.
 */
{
    int save;
    const char *action = [[sender selectedCell] title];

    if (!action || !*action) action = NXLocalString("Close", NULL, "The operation of closing a window usually generated by choosing the Close Window option in the Windows menu.");

    if ([self isDirty]) {
	if (cancellable) {
	    save = NXRunAlertPanel(action, SAVE_CHANGES, SAVE, DONT_SAVE, CANCEL, name);
	} else {
	    save = NXRunAlertPanel(action, SAVE_CHANGES, SAVE, DONT_SAVE, NULL, name);
	}
	if (save != NX_ALERTDEFAULT && save != NX_ALERTALTERNATE) {
	    return nil;
	} else {
	    [window endEditingFor:self];	/* terminate any editing */
	    if ((save == NX_ALERTDEFAULT) && ![self save:sender]) return nil;
	}
    }

    return self;
}

- close
{
    [linkManager documentClosed];
    [linkManager free];
    linkManager = nil;
    [self reset:self];
    [NXApp delayedFree:self];
    return self;
}

- windowWillClose:(Window *)sender
{
    if ([self windowWillClose:nil cancellable:YES]) {
	return [self close];
    } else {
	return nil;
    }
}

- windowDidBecomeMain:(Window *)sender
/*
 * Switch the Application's PrintInfo to the document's when the document
 * window becomes the main window.  Also set the cursor appropriately
 * depending on which tool is currently selected.
 */
{
    [NXApp setPrintInfo:printInfo];
    [self resetCursor];
    return self;
}

- windowDidUpdate:(Window *)sender
{
    if ([window isMainWindow]) [view updateLinksPanel];
    return self;
}

- windowWillResize:(Window *)sender toSize:(NXSize *)size
/*
 * Constrains the size of the window to never be larger than the
 * GraphicView inside it (including the ScrollView around it).
 */
{
    NXRect fRect, cRect;

    getContentSizeForView(view, &cRect.size);
    [[window class] getFrameRect:&fRect forContentRect:&cRect style:[window style]];
    if ([[window contentView] horizontalRulerIsVisible]) fRect.size.height += [Ruler width];
    if ([[window contentView] verticalRulerIsVisible]) fRect.size.width += [Ruler width];
    size->width = MIN(fRect.size.width, size->width);
    size->height = MIN(fRect.size.height, size->height);
    size->width = MAX(MIN_WINDOW_WIDTH, size->width);
    size->height = MAX(MIN_WINDOW_HEIGHT, size->height);

    return self;
}

- windowDidResize:(Window *)sender
/*
 * Just makes sure the selection is visible after resizing.
 */
{
    [view scrollSelectionToVisible];
    return self;
}

- windowWillMiniaturize:(Window *)sender toMiniwindow:counterpart
{
    char *dot;
    char title[MAXPATHLEN+1];

    strcpy(title, [self name]);
    dot = strrchr(title, '.');
    if (dot && !strcmp(dot, ".draw")) *dot = '\0';
    [counterpart setTitle:title];
    return self;
}

- windowWillReturnFieldEditor:(Window *)sender toObject:client
{
    if (!undoFieldEditor) undoFieldEditor = [[UndoText alloc] initFrame:NULL];
    return undoFieldEditor;
}

/* Validates whether a menu command makes sense now */

#define HIDE_RULER NXLocalString("Hide Ruler", NULL, "Menu item which hides the ruler.")
#define SHOW_RULER NXLocalString("Show Ruler", NULL, "Menu item which unhides the ruler.")

- (BOOL)validateCommand:(MenuCell *)menuCell
/*
 * Validates whether a menu command that DrawDocument responds to
 * is valid at the current time.
 */
{
    SEL action = [menuCell action];

    if (action == @selector(save:)) {
	return YES;
    } else if (action == @selector(revertToSaved:)) {
	return ([self isDirty] && haveSavedDocument);
    } else if (action == @selector(saveAs:)) {
	return (haveSavedDocument || ![view isEmpty]);
    } else if (action == @selector(saveTo:)) {
	return ![view isEmpty];
    } else if (action == @selector(saveLink:)) {
	return (haveSavedDocument && ![view hasEmptySelection]);
    } else if (action == @selector(close:)) {
	return YES;
    } else if (action == @selector(hideRuler:)) {
	if ([[window contentView] eitherRulerIsVisible] && strcmp([menuCell title], HIDE_RULER)) {
	    [menuCell setTitle:HIDE_RULER];
	    [menuCell setEnabled:NO];
	} else if (![[window contentView] eitherRulerIsVisible] && strcmp([menuCell title], SHOW_RULER)) {
	    [menuCell setTitle:SHOW_RULER];
	    [menuCell setEnabled:NO];
	}
    } else if (action == @selector(alignSelLeft:) ||
	       action == @selector(alignSelRight:) ||
	       action == @selector(alignSelCenter:) ||
	       action == @selector(checkSpelling:) ||
	       action == @selector(showGuessPanel:)) {
	return [[window getFieldEditor:NO for:NXApp] superview] ? YES : NO;
    }

    return [super validateCommand:menuCell];
}

/* Cursor-setting method */

- resetCursor
/*
 * Sets the document's cursor according to whatever the current graphic is.
 * Makes the graphic view the first responder if there isn't one or if
 * no tool is selected (the cursor is the normal one).
 */
{
    id fr, cursor = [NXApp cursor];
    ScrollView *scrollview = [window contentView];

    [scrollview setDocCursor:cursor];
    fr = [window firstResponder];
    if (!fr || fr == window || cursor == NXArrow) {
	[window makeFirstResponder:view];
    }

    return self;
}

@end
